/*
Copyright (c) 2009, Young Digital Planet SA
All rights reserved.

* Redistribution and use in source and binary forms, with or without 
  modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this 
  list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, 
  this list of conditions and the following disclaimer in the documentation 
  and/or other materials provided with the distribution.
* Neither the name of the Young Digital Planet SA nor the names of its 
  contributors may be used to endorse or promote products derived from this 
  software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
POSSIBILITY OF SUCH DAMAGE.
*/
package pl.ydp.utils
{
	import flash.errors.IllegalOperationError;
	import flash.events.Event;
	import flash.events.IEventDispatcher;
	import pl.ydp.utils.FuncUtil;
	
	/**
	 * Zadaniem klasy jest ułatwienie zarządzania eventami przychodzącymi
	 * z kilku źródeł równocześnie.
	 * To jest druga wersja z ułatwionym uzywaniem.
	 * <p>
	 * Pierwotnym katalizatorem powstania tej klasy było ładowanie plików 
	 * z kilku źródeł naraz, obsługa załądowania wszystkich oraz ew. błedów 
	 * okazała się bagnem. Klasa ta ma upraszczać takie przypadki. Przy jej 
	 * pomocy tworzy się łańcuch event'ów, określając jakie eventy muszą się
	 * wydarzyć (można tworzyć komplementarne grupy, wtedy wystarczy, że jeden
	 * ze zgrupowanych event'ów się wydarzy), podpina się funkcje, które
	 * zostaną wywołaniu po przyjęciu event'ów oraz ew. handlery błędów.
	 * Załóżmy, że mamy n źródeł eventów (np. loaderów) e1 ... en 
	 * zarządzamy nimi  w ten sposób:
	 * <pre>
	 * chain:EventExecutionChain2
	 * <font color="#009900">// rejestruję obsługę eventów przez chain</font>
	 * chain.beginGroup()
	 * chain.scheduleEvent( e1, "loaded", EventExecutionChain.SKIP )
	 * chain.scheduleEvent( e1, "failed", EventExecutionChain.THROW_EVENT )
	 * chain.endGroup()
	 * chain.beginGroup()
	 * chain.scheduleEvent( e2, "loaded", EventExecutionChain.SKIP )
	 * chain.scheduleEvent( e2, "failed", EventExecutionChain.THROW_EVENT )
	 * chain.endGroup()
	 * .....
	 * chain.beginGroup()
	 * chain.scheduleEvent( en, "loaded", EventExecutionChain.SKIP )
	 * chain.scheduleEvent( en, "failed", EventExecutionChain.THROW_EVENT )
	 * chain.endGroup()
	 * <font color="#009900">// rejestruje teraz metodę, która zostanie zawołana po obsłużeniu
	 * // wszystkich zarejestrowanych wcześniej eventów</font>
	 * chain.schedule( everythingLoaded/:Function/ );
	 * <font color="#009900">// rejestruje teraz metodę, która zostanie zawoła gdy w łańcuchu
	 * // zdarzeń zostanie rzucony wyjątek - wszystkie metody/eventy od
	 * // momentu pojawienia się eventu do tego miejsca zostaną pominięte</font>
	 * chain.scheduleCatch( somethingWentWrong/:Function/ );
	 * <font color="#009900">// startuję łańcuch</font>
	 * chain.start()
	 * e1.load();
	 * e2.load();
	 * </pre>
	 * Dzięki takiemu podejściu obsługa sukcesu jak i błędu zebrana jest w 
	 * jednym miejscju, nie potrzeba liczyć eventów, chain także sam 
	 * odrejestrowuje event listener'y grup jak i w wypadku pojawienia się
	 * wyjątku.
	 * Ponadto, jeżeli w 
	 * łańuchu wołań wołana metoda zarejestruje własne metody lub eventy to 
	 * metody będące w łańcuchu
	 * po niej zostaną wołane po zarejestowanych przez nią funkcjach.
	 * @see #schedule()
	 * @see #scheduleCallback()
	 * @see #scheduleCatch()
	 * @see #scheduleEvent()
	 * @see #start()
	 */
	public class EventExecutionChain2
	{
		/**
		 * Function constant to skip event. No paramter.
		 */
		public static function SKIP( ... args ):void {
		}
		/**
		 * Function that causes, to throw exception into chain.
		 * Requires chain paramter.
		 * @deprecated
		 */
		public static function THROW( e:Error, eec:EventExecutionChain2 ):void {
			eec.doThrow( e );
		}

		/**
		 * Function receives an event and throws ErrorEvent into a chain.
		 * @see ErrorEvent
		 */
		public static function THROW_EVENT( e:Event ):void {
			throw new EventError(e);
		}
		/**
		 * Kolejka zarejestrowanych metod i tokenów do callback'ów.
		 */
		internal var m_methods:Array;
		/**
		 * Tymczasowa tablica zbierająca rejestrację metod podczas 
		 * realizacji wołania. Po wołaniu zebrane metody są wstawiane
		 * na początek kolejki m_methods
		 * @see #m_methods
		 */
		internal var callbackGatherer:Array = null;
		
		/**
		 * Inizjalizacja.
		 * @param methods opcjonalnie, metody, które zostaną zarejestrowane 
		 * do wykonania
		 */
		public function EventExecutionChain2( ... methods ) {
			m_methods = methods.concat();
		}
		
		/**
		 * Zarejestrowanie pod-łańcucha, elementy zarejestrowane w tym łańcuchu
		 * w tej chwili, muszą się zakończyć, nie będą wołane metody zarejestrowane
		 * po nim. Wyjątki rzucone w pod-łańcuchu zostaną przechwycone i 
		 * propagowane w tym łanuchu (this).
		 * @param chain2 pod łańcuch, na który ten (this) łańcuch będzie czekał
		 */
		public function chain( chain2:EventExecutionChain2 ):void {
			chain2.schedule( scheduleCallback(SKIP) );
			chain2.scheduleCatch( doThrow );
		}
		
		/**
		 * Zarejestrowanie metody. Zostanie ona wywołana po wyjściu z wołającej
		 * funkcji ale po zarejestrowanych przez tą funkcja callbackach i metodach.
		 * @param method metoda, która ma zostać wywołana
		 * @param args dodatkowe parametry, które zostaną przekazane do metody
		 */
		public function schedule( method:Function, ... args ):void {
			var aargs:Array = args.concat();
			scheduleMethods( FuncUtil.UnshiftArgumentsArray(method, args) );
		} 
		
		/**
		 * Zarejestrowanie kilku metod naraz.
		 * @param methods metody do wywołania
		 */
		public function scheduleMethods( ... methods ):void {
			var tg:Array = callbackGatherer===null ? m_methods : callbackGatherer;
			for each ( var method:* in methods )
				tg.push( method );
		}
		
		/**
		 * Throw exception into chain, all functions and callback
		 * will be skipped until catch handler will be reached. The
		 * same effect can be reached when you throw a exception in
		 * called function or callback.
		 * @param x exception to throw
		 * @see #scheduleCatch()
		 */
		public function doThrow( x:Error ):void {
			while( x!=null && m_methods.length>0 ) {
				var call:* = m_methods.shift();
				if ( call.catchHandler!=undefined ) {
					call.catchHandler( x );
					x = null;
				} else
					skipCall( call );
			}
			
			if ( x!=null )
				trace( x.message+"\n"+x.getStackTrace() );
		}
		
		/**
		 * Add catch handler that catches execption throw into chain.
		 * After catch next functions are called normally.
		 * @param catchHandler function called when exception is
		 * thrown into chain. Function prototype
		 * <code>function catchHandler( e:Error ):void</code>.
		 */
		public function scheduleCatch( catchHandler:Function ):void {
			scheduleMethods( { catchHandler: catchHandler } );
		}
		
		private var _groupCallbacks:Boolean = false;
		private var _currentToken:* = null;
		/**
		 * Start callbacks group. When one function in group
		 * is called chain can call next function. It's usable
		 * when you have to register callback on success and
		 * callback on failure. 
		 * @see #endCallbackGroup()
		 */ 
		public function beginCallbackGroup():void {
			if ( _groupCallbacks==true )
				throw new IllegalOperationError( "Overrlapping callbacks groups are disabled" );
			
			_groupCallbacks = true;
		}
		
		/**
		 * End group
		 * @see #beginCallbackGroup()
		 */
		public function endCallbackGroup():void {
			_groupCallbacks = false;
			_currentToken = null;
		}
		/**
		 * Start callbacks group. When one function in group
		 * is called chain can call next function. It's usable
		 * when you have to register callback on success and
		 * callback on failure. Short name for beginCallbackGroup
		 * @see #endGroup()
		 * @see #beginCallbackGroup()
		 */ 
		public function beginGroup():void {
			beginCallbackGroup();
		}
		
		/**
		 * End group.  Short name for beginCallbackGroup
		 * @see #beginGroup()
		 */
		public function endGroup():void {
			endCallbackGroup();
		}
		
		protected function getCallbackToken():* {
			if ( !_groupCallbacks ) {
				return _createCallbackToken();
			}
			else {
				if ( _currentToken==null )
					_currentToken = _createCallbackToken();
				return _currentToken;
			}
		}
		
		protected function _createCallbackToken():* {
			var token:* = { eventToken: true };
			scheduleMethods( token );
			return token;
		}
		
		/**
		 * Zarejestrowanie callbacka. Metody rejestrowane po tym callbacku będą 
		 * wołane gdy ten callback zostanie obsłużony. Can be groupped.
		 * @see #beginGroup()
		 * @param callback callback, that should be called
		 * @param args dodatkowe argumenty. Zostaną wstawione na
		 * końcu listy argumentów przy wołaniu f-ji callback
		 * @return funkcja która została skolejkowana i wywoła przekazanego callbacka
		 */
		public function scheduleCallback(callback:Function, ...args):Function{
			var token:* = getCallbackToken();
				
			callback = FuncUtil.AddArgumentsArray( callback, args );
			var me:EventExecutionChain2 = this;
			var fn:Function = function( ... rargs ):void {
				me.performCall( callback, rargs );
				me.callbackHandled(token);
			};
			
			return fn;
		}
		
		/**
		 * Zarejestrowanie eventu. Metody rejestrowane po tym evencie będą 
		 * wołane gdy ten event zostanie obsłużony. This is similar functionality
		 * to scheduleCallback, but event listener will be unregistered when chain
		 * moves forward. Can be groupped.
		 * @see #beginCallbackGroup()
		 * @param source źródło eventu
		 * @param type typ eventu
		 * @param callback funkcja handler eventu
		 * @param additionalArgs dodatkowe argumenty. Zostaną wstawione na
		 * początku listy argumentów przy wołaniu f-ji callback
		 */
		public function scheduleEvent( source:IEventDispatcher, type:String, callback:Function, ... additionalArgs ):void {
			// gdy nie podano token'a tworzę nowy
			var token:* = getCallbackToken();
			if ( !token.hasOwnProperty("registars") )
				token.registars = [];
			
			callback = FuncUtil.AddArgumentsArray( callback, additionalArgs );
			var me:EventExecutionChain2 = this;
			var eFn:Function = function(evt:Event):void {
				me.performCall( callback, [evt] );
				me.callbackHandled( token );
			};
			token.registars.push( { source: source, type: type, fn: eFn } ); 
			source.addEventListener( type, eFn );
		}
		
		/**
		 * Zwraca true jeżeli handler aktualnie czeka na zwrotne wołanie
		 * z eventu.
		 */
		public function waitsForEvent():Boolean {
			return m_methods.length>0 && m_methods[0].eventToken;
		}
		
		/**
		 * Rozpoczęcie wywołań funkcji. Metoda wraca po wywołaniu
		 * wszystkich metod lub gdy wykonanie jest wstrzymane w 
		 * oczekiwaniu na event.
		 */
		public function start():void {
			callAll();
		}
		
		/**
		 * Metoda wołana po obsłudze eventu. Ma za zadanie
		 * wyrejsetrować go i puścić dalej wykonywanie
		 * zadań
		 */
		internal function callbackHandled(eventToken:Object):void{
			unregisterToken( eventToken );
			
			var eix:int = m_methods.indexOf( eventToken );
			
			if ( eix==-1 ) {
				if ( callbackGatherer ) {
					// może się zdażyć, że event zostanie wywołany nim
					// trafi do głównej kolejki
					eix = callbackGatherer.indexOf( eventToken );
					if ( eix>=0 )
						callbackGatherer.splice( eix, 1 );
					else {
						// nie ma token'a - zapewne został usnięty przy exception handling
						callAll();
					}
				} else {
					// nie ma callbackGatherer'a, nie ma tokenu, czołg idzie dalej
					callAll();
				}
			} else {
				// event wywołany
				m_methods.splice( eix, 1 );
				// wykonywanie idzie dalej
				callAll();
			}
		}
		
		/**
		 * Wykonanie zadanej metody w kontekście łańcucha. Gdy metoda
		 * doda do łańcucha jakieś eventy zostaną one dodane na początku
		 * łańcucha wołań (z zachowaniem kolejności wołań podaną przez
		 * funkcję)
		 */
		internal function performCall( target:Function, arg:Array = null ):void {
			// inicjuję zbieracza metod, dodawane eventy będą 
			// lądowały w num
			callbackGatherer = [];
			
			try {
				if ( arg==null )
					target();
				else
					target.apply( null, arg );
			} catch( x:Error ) {
				for each( var call:* in callbackGatherer )
					skipCall(call);
				doThrow( x );
			}
				
			// przenoszę nowo dodane metody do głównej kolejki
			for( var i:int=callbackGatherer.length-1; i>=0; --i )
				m_methods.unshift( callbackGatherer[i] );
			callbackGatherer = null;
		}
		
		protected function unregisterToken( eventToken:* ):void {
			if ( eventToken.hasOwnProperty("registars") ) {
				for each( var o:* in eventToken.registars )
					IEventDispatcher(o.source).removeEventListener( o.type, o.fn );
				delete eventToken.registars;
			}
		}
		
		protected function skipCall( call:* ):void {
			if ( call.eventToken )
				unregisterToken( call );
		}
		
		/**
		 * Wykonanie wołań.
		 */
		internal function callAll():void {
			while( m_methods.length>0 && !waitsForEvent() ) {
				var call:* =  m_methods.shift();
				if ( call.catchHandler==undefined )
					performCall( call );
			}
		}
	}
}
